Activating project at `/workspaces/Kalman filtering and smoothing`
Updating registry at `~/.julia/registries/General.toml`
Resolving package versions...
No Changes to `/workspaces/Kalman filtering and smoothing/Project.toml`
No Changes to `/workspaces/Kalman filtering and smoothing/Manifest.toml`
Pkg.status()
Status `/workspaces/Kalman filtering and smoothing/Project.toml`
[6e4b80f9] BenchmarkTools v1.8.0
[31c24e10] Distributions v0.25.131
[b964fa9f] LaTeXStrings v1.4.1
[91a5bcdd] Plots v1.41.7
⌅ [86711068] RxInfer v3.10.1
[860ef19b] StableRNGs v1.0.4
Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated`
Kalman filtering and smoothing (Part 1)
This is an analysis of the RxInfer example at https://examples.rxinfer.com/categories/basic_examples/kalman_filtering_and_smoothing/
Some symbols have been changed
Some content has been added/modified
The preference is to make the math and code names align as much as possible
where \(\mathbf{x}_t\) are hidden states, \(\mathbf{y}_t\) are noisy observations, \(\mathbf{A}\), \(\mathbf{C}\) are state transition and observation matrices, \(\mathbf{\Sigma_x}\) and \(\mathbf{\Sigma_y}\) are state transition noise and observation noise covariance matrices. For a more rigorous introduction to Linear Gaussian Dynamical systems we refer to Simo Sarkka, Bayesian Filtering and Smoothing book.
seed =1234rng =MersenneTwister(seed)## We will model 2-dimensional observations with rotation matrix `Aˣ`## To avoid clutter we also assume that matrices `Aˣ`, `Cˣ`, `Σˣₓ`, and `Σˣᵧ`## are known and fixed for all time-steps_xˣ₀ = [ 10.0, -10.0 ]θˣ =π/35_Aˣ = [ cos(θˣ) -sin(θˣ); sin(θˣ) cos(θˣ) ]_Cˣ =diageye(2)_Σˣₓ =diageye(2)_Σˣᵧ =25.0.*diageye(2)T =300; ## number of observations
Next step, is to generate some synthetic data.
The Generative Process
State transition function (\(f_E\))
The state transition function provides the deterministic part of the state flow. The probabilistic part is provided by the system noise:
## Data comes from either a simulation/lab (sim|lab) OR from the field (fld)## Data are handled either in batches (batch) OR online as individual points (point)## Batch data accumulates either## along the depth/examples dimension/axis (into the screen/page), OR## typical for supervised & unsupervised learning## along the time dimension/axis (down the screen page)## typical for sequential decision learning (reinforcement learning & active inference)functionsim_batch_data(rng, T, _Aˣ, _Cˣ, _Qˣ, _Rˣ) ## simulated batch data _xˣₜ₋₁ = _xˣ₀ _fEː =Vector{Vector{Float64}}(undef, T) _xˣː =Vector{Vector{Float64}}(undef, T) _gEː =Vector{Vector{Float64}}(undef, T) _yˣː =Vector{Vector{Float64}}(undef, T)for t in1:T _fEː[t] =fE(_Aˣ=_Aˣ, _xˣₜ₋₁=_xˣₜ₋₁) _xˣː[t] =rand(rng, MvNormal(_fEː[t], _Σˣₓ)) _gEː[t] =gE(_Cˣ=_Cˣ, _xˣₜ=_xˣː[t]) _yˣː[t] =rand(rng, MvNormal(_gEː[t], _Σˣᵧ)) _xˣₜ₋₁ = _xˣː[t]endreturn _xˣː, _yˣːend
Let’s plot our synthetic dataset. Lines represent our hidden states we want to estimate using noisy observations, which are represented as dots.
p =plot(title="Hidden states with noisy observations")p =plot!(p, getindex.(_xˣː, 1), label="Hidden Signal "* L"x^*_1", color=:red)p =scatter!(p, getindex.(_yː, 1), label=false, markersize=2, color=:red)p =plot!(p, getindex.(_xˣː, 2), label="Hidden Signal "* L"x^*_2", color=:blue)p =scatter!(p, getindex.(_yː, 2), label=false, markersize=2, color=:blue)plot(p)
The Generative Model
To create a model we use GraphPPL package and @model macro:
@modelfunctionrotate_ssm(_yː, _x₀, _A, _C, _Σₓ, _Σᵧ) _x_prior ~MvNormalMeanCovariance(mean(_x₀), cov(_x₀)) _xₜ₋₁ = _x_priorfor t in1:length(_yː) _xː[t] ~MvNormalMeanCovariance(_A*_xₜ₋₁, _Σₓ) ## `_x` is a sequence of hidden states _yː[t] ~MvNormalMeanCovariance(_C*_xː[t], _Σᵧ) ## `_y` is a sequence of "clamped" observations _xₜ₋₁ = _xː[t]endend
To run inference we also specify a prior for our first hidden state:
## For large number of observations you need to use limit_stack_depth = 100 option during model creation, e.g. ## infer(..., options = (limit_stack_depth = 500, ))`## We assume the Aˣ, Cˣ, Σˣₓ, Σˣᵧ are known, i.e. not hidden result =infer( model=rotate_ssm(_x₀=_xˣ₀, _A=_Aˣ, _C=_Cˣ, _Σₓ=_Σˣₓ, _Σᵧ=_Σˣᵧ), data= (_yː = _yː,), free_energy=true);
xmarginals = result.posteriors[:_xː]logevidence =-result.free_energy; ## given the analytical solution, free energy will be equal to the negative log evidence
p =plot(title="Estimated states from noisy observations")p =plot!(p, getindex.(_xˣː, 1), label="Hidden Signal "* L"x^*_1", color=:red, linestyle=:dash)p =plot!(p, getindex.(_xˣː, 2), label="Hidden Signal "* L"x^*_2", color=:blue, linestyle=:dash)p =plot!(p, getindex.(mean.(xmarginals), 1), ribbon=getindex.(var.(xmarginals), 1) .|> sqrt, fillalpha=0.5, label="Estimated Signal "* L"x_1", color=:pink)p =plot!(p, getindex.(mean.(xmarginals), 2), ribbon=getindex.(var.(xmarginals), 2) .|> sqrt, fillalpha=0.5, label="Estimated Signal "* L"x_2", color=:lightblue)plot(p)
As we can see from our plot, estimated signal resembles closely to the real hidden states with small variance. We maybe also interested in the value for minus log evidence: